//************************************************************
//    > File Name: biliat.cpp
//    > Author: jiayue
//    > Mail: tjyemail@sina.com 
//    > Created Time: 2019年07月10日 星期三 09时22分23秒
//    function :
//    archive bilibili download videos to mp4
//    download -> avidxxx -> 1 -> lua.flv.xxx -> 0.blv
//    run : biliat -s download -a copy -t ffmpeg
//***********************************************************

#include<bits/stdc++.h>
#include<unistd.h>
#include<dirent.h>

// different os
#ifdef _WIN32
// windows all
#include<io.h>
#include<windows.h>
static const char BILI_TOOL_OS[]="Windows";
static const char BILI_PATH_COM='\"';
static const char BILI_PATH_SPLIT='\\';
static const char BILI_PATH_ILLEGAL[]="/\\:?*\"\'<>|";
static const char BILI_CMD_MV[]="move";
static const char BILI_CMD_RM[]="del";
	#ifdef _WIN64
	// windows x64
	#else
	// windows x86
	#endif

#elif __APPLE__
#include "TargetConditionals.h"
static const char BILI_TOOL_OS[]="Mac OS X";
static const char BILI_PATH_COM='\'';
static const char BILI_PATH_SPLIT='/';
static const char BILI_PATH_ILLEGAL[]="/\\:?*\"\'<>|";
static const char BILI_CMD_MV[]="mv";
static const char BILI_CMD_RM[]="rm";
	#ifdef TARGET_OS_IPHONE
	#elif TARGET_OS_MAC
	#endif

#elif __linux__
#include<unistd.h>
#include<dirent.h>
static const char BILI_TOOL_OS[]="Linux";
static const char BILI_PATH_COM='\'';
static const char BILI_PATH_SPLIT='/';
static const char BILI_PATH_ILLEGAL[]="/\\:?*\"\'<>|";
static const char BILI_CMD_MV[]="mv";
static const char BILI_CMD_RM[]="rm";

#elif __ANDROID__
static const char BILI_TOOL_OS[]="Android";
static const char BILI_PATH_COM='\'';
static const char BILI_PATH_SPLIT='/';
static const char BILI_PATH_ILLEGAL[]="/\\:?*\"\'<>|";
static const char BILI_CMD_MV[]="mv";
static const char BILI_CMD_RM[]="rm";

#endif

using namespace std;

// const vars
static const char BILI_TOOL_VER[]="Bilibili Archive Tool 2.0";
static const int BILI_PATH_LEN = 2048;
static const char BILI_FFMPEG_AV2MP4[]="%s -i \"%s\" -i \"%s\" -c copy \"%s.mp4\"";
static const char BILI_FFMPEG_ALL2MP4[]="%s -i \"%s\" -c copy \"%s.mp4\"";
static const char BILI_FFMPEG_MERGE2MP4[]="%s -f concat -i \"%s\" -c copy \"%s.mp4\"";

// need parse params
string bili_current_dir_abs;
string bili_source_dir;
string bili_action="copy";
string bili_trans_tool="ffmpeg";
string bili_property_type="all";
string bili_trans_cmd;

// utility function
void trim_end(string &path, char trim){
	for(; path.length() && path.back()==trim; path.pop_back());
	if(path.length()==0) path.push_back('/');
}

void get_current_path_abs(string &path){
	char buf[BILI_PATH_LEN];
	getcwd(buf,BILI_PATH_LEN);
	path=buf;
}

void get_path_abs(string &path){
	string last;
	get_current_path_abs(last);
	chdir(path.c_str());
	get_current_path_abs(path);
	chdir(last.c_str());
}

#ifdef _WIN32
void utf8_to_gbk(const char *org, char *dest, int size){
	wchar_t tmp[6*2048];
	MultiByteToWideChar(CP_UTF8,0,org,-1,tmp,sizeof(tmp));
	WideCharToMultiByte(CP_ACP,0,tmp,-1,dest,size,NULL,NULL);
}
#endif

void remove_str(string &name, const char *illegal){
	for(int i=0; i<name.length();){
		if(strchr(illegal,name[i]))
			name.erase(i,1);
		else ++i;
	}
}

int file_exists(const string &file){
	FILE *fp = fopen(file.c_str(),"r");
	if(fp==NULL) return 0;
	fclose(fp);
	return 1;
}

void remove_str(char *name, const char *illegal){
	string tmp=name;
	remove_str(tmp,illegal);
	strcpy(name,tmp.c_str());
}

static void help(const char *h)
{
	//printf("%s [%s]\n",BILI_TOOL_VER, BILI_TOOL_OS);
	if(h) printf("bad param : %s\n", h);
	printf("Usages\t:\n");
	printf("\t-h     \t: show this help\n");
	printf("\t-s DIR \t: source(origin) bili download dir, NOT NULL\n");
	//printf("\t-e ENC \t: encode format of result [utf-8 | gbk | gb2312]\n");
	printf("\t-a ACT \t: action - move/copy: delete/copy each dirs when finish\n");
	printf("\t-t TOOL\t: tool path of ffmpeg(Optional)\n");
	printf("\t-p PROP\t: biliapp property: all/av/merge(default all)\n");
	printf("\t-c CMD \t: tool trans command(Optional)\n");
	printf("Example :\n");
	printf("      1 : biliat -s download -a move\n");
	printf("      2 : biliat -s download -a copy -t C:\\ffmpeg.exe\n");
	printf("      3 : biliat -s download -a copy %s\n","-c copy .mkv");

	exit(0);
}

static int parser_param(int argc, char* argv[]){
	if(argc==0)	help(NULL);
	for (int i=0; i<argc; ++i){
		if (strlen(argv[i])<2 || argv[i][0]!='-') help(argv[i]);
		switch(argv[i][1]){
			case 'h': help(NULL);
			case 's': // set bili source dir
					  if (i+1==argc) help(argv[i]);	// bad param, exit
					  bili_source_dir = argv[++i];
					  break;
			case 'a': // set do action
					  if (i+1==argc) help(argv[i]);	// bad param, exit
					  bili_action = argv[++i];
					  if (bili_action.compare("move")&&bili_action.compare("copy"))
						  help(argv[i]);				// bad param, exit
					  break;
			case 't': // set tool path
					  if (i+1==argc) help(argv[i]);	// bad param, exit
					  bili_trans_tool = argv[++i];
					  break;
			case 'p': // set biliapp property
					  if (i+1==argc) help(argv[i]);	// bad param, exit
					  bili_property_type = argv[++i];
					  break;
			case 'c': // TODO set tool command
					  if (i+1==argc) help(argv[i]);	// bad param, exit
					  bili_trans_cmd = argv[++i];
					  break;
			default : help(argv[i]);			// bad param, exit
					  break;
		}
	}
	return 0;
}

// declare function
void list_bili_source(const string &path);
int list_avid_dir(const string &path);
int parser_part(const string &path);
int parser_json(const string &json, const string match, string &data);
int enum_blv(const string &file_parent, const string &file_type, const string &file_newname);
int enum_av(const string &file_parent, const string &file_type, const string &file_newname);


// working...
// list dir av-id xxxxx in bili source dir
#ifdef _WIN32
void list_bili_source(const string &path){
	int avid_cnt = 0;	// all av-id dirs count
	int video_cnt = 0;	// all video count
	string dir_find=path+BILI_PATH_SPLIT+"*";
	_finddata_t file;
	intptr_t handle =_findfirst(dir_find.c_str(), &file);
	if(handle == -1) return ;
	do{
		if(!(file.attrib & _A_SUBDIR)) continue;
		if(!strcmp(file.name,".") || !strcmp(file.name,"..")) continue;
		// list current av-id dir
		string avid_dir = path+BILI_PATH_SPLIT+string(file.name);
		int part_cnt = list_avid_dir(avid_dir);
		if(part_cnt<1) continue;	// may error cause by merge

		++avid_cnt;
		video_cnt+=part_cnt;
	}while(_findnext(handle, &file)==0);
	_findclose(handle);
	cout<<"### AVID : "<<avid_cnt<<" , VIDEO : "<<video_cnt<<endl;
}

#elif __linux__
void list_bili_source(const string &path){
	int avid_cnt = 0;	// all av-id dirs count
	int video_cnt = 0;	// all video count
	DIR *dir=opendir(path.c_str());
	if(dir==NULL) return ;
	for(dirent *file; file=readdir(dir);){
		if(file->d_type != 4) continue;
		if(!strcmp(file->d_name,".") || !strcmp(file->d_name,"..")) continue;
		// list current av-id dir
		string avid_dir = path+BILI_PATH_SPLIT+string(file->d_name);
		int part_cnt = list_avid_dir(avid_dir);
		if(part_cnt<1) continue;	// may error cause by merge

		++avid_cnt;
		video_cnt+=part_cnt;
	}
	closedir(dir);
	cout<<"### AVID : "<<avid_cnt<<" , VIDEO : "<<video_cnt<<endl;
}

#endif


// list 1/2/3/... in avid xxxx dir
#ifdef _WIN32
int list_avid_dir(const string &path){
	int part_cnt = 0;
	string dir_find=path+BILI_PATH_SPLIT+"*";
	_finddata_t file;
	intptr_t handle =_findfirst(dir_find.c_str(), &file);
	if(handle == -1) return part_cnt;
	do{
		if(!(file.attrib & _A_SUBDIR)) continue;
		if(!strcmp(file.name,".") || !strcmp(file.name,"..")) continue;
		// list current part dir
		string part_dir = path+BILI_PATH_SPLIT+string(file.name);
		int parser_cnt = parser_part(part_dir);
		if(parser_cnt<1) continue;	// may error cause by merge
		++part_cnt;
	}while(_findnext(handle, &file)==0);
	_findclose(handle);

	return part_cnt;
}

#elif __linux__
int list_avid_dir(const string &path){
	int part_cnt = 0;
	DIR *dir=opendir(path.c_str());
	if(dir==NULL) return part_cnt;
	for(dirent *file; file=readdir(dir);){
		if(file->d_type != 4) continue;
		if(!strcmp(file->d_name,".") || !strcmp(file->d_name,"..")) continue;
		// list current part dir
		string part_dir = path+BILI_PATH_SPLIT+string(file->d_name);
		int parser_cnt = parser_part(part_dir);
		if(parser_cnt<1) continue;	// may error cause by merge
		++part_cnt;
	}
	closedir(dir);
	return part_cnt;
}

#endif

// parser video info in 1/2/3/...
int parser_part(const string &path){
	const string entry_json=path+BILI_PATH_SPLIT+"entry.json";
	string json_data;
	ifstream ifs(entry_json);
	getline(ifs,json_data);
	// parser json
	string file_title;
	string file_part_title;
	string file_tag;
	parser_json(json_data,"\"title\"",file_title);
	parser_json(json_data,"\"part\"",file_part_title);
	parser_json(json_data,"\"type_tag\"",file_tag);
	remove_str(file_title,BILI_PATH_ILLEGAL);
	remove_str(file_part_title,BILI_PATH_ILLEGAL);
	if(file_title.length()+file_part_title.length()>250){
		if(file_part_title.length()>10) file_part_title=file_part_title.substr(0,10);
		else file_title=file_title.substr(0,250-file_part_title.length());
	}

	const string file_type=bili_trans_cmd.length()?
		bili_trans_cmd.substr(bili_trans_cmd.find_last_of(".")):".mp4";
	if(bili_trans_cmd.length()) bili_trans_cmd=bili_trans_cmd.substr(0,bili_trans_cmd.find_last_of("."));
	const string file_parent = path+BILI_PATH_SPLIT+file_tag;
	const string file_newname= bili_source_dir+BILI_PATH_SPLIT+
		file_title+"_"+file_part_title+file_type;

	if(bili_property_type.compare("all")==0)
		return enum_blv(file_parent,file_type,file_newname);
	else if(bili_property_type.compare("av")==0)
		return enum_av(file_parent,file_type,file_newname);
	return 0;
}

int enum_blv(const string &file_parent, const string &file_type, const string &file_newname){
	// emum 0.blv, 1.blv, ...
	char cmd[2048*4];
	int parser_cnt=0;
	for(int i=0; i<10; ++i){
		const string file_org = file_parent+BILI_PATH_SPLIT+to_string(i)+".blv";
		const string file_dest= file_parent+BILI_PATH_SPLIT+to_string(i);	// cmd has file type
		if(file_exists(file_org)==0) continue;

		++parser_cnt;
		// ffmpeg trans
		if(bili_trans_cmd.length()){
			sprintf(cmd,"%s -i %c%s%c %s %c%s%s%c",
					bili_trans_tool.c_str(),
					BILI_PATH_COM,file_org.c_str(),BILI_PATH_COM,
					bili_trans_cmd.c_str(),
					BILI_PATH_COM,file_dest.c_str(),file_type.c_str(),BILI_PATH_COM);
		}
		else{
			sprintf(cmd,BILI_FFMPEG_ALL2MP4,
					bili_trans_tool.c_str(),
					file_org.c_str(),
					file_dest.c_str());
		}
		printf("cmd : %s\n",cmd);
		system(cmd);
		// move
		sprintf(cmd,"%s %c%s%s%c %c%s%c",BILI_CMD_MV,
				BILI_PATH_COM,file_dest.c_str(),file_type.c_str(),BILI_PATH_COM,
				BILI_PATH_COM,file_newname.c_str(),BILI_PATH_COM);
#ifdef _WIN32
		utf8_to_gbk(cmd,cmd,sizeof(cmd));
		remove_str(cmd,"?");
#endif
		if(file_exists(file_newname)) {
			// duplicate file_name
			cmd[strlen(cmd)-file_type.length()-1]='\0';
			sprintf(cmd,"%s_%d_%s%c",cmd,i,file_type.c_str(),BILI_PATH_COM);
		}
		printf("cmd : %s\n",cmd);
		system(cmd);
		// delete *.blv
		if(bili_action.compare("move")==0){
			sprintf(cmd,"%s %c%s%c",BILI_CMD_RM,
					BILI_PATH_COM,file_org.c_str(),BILI_PATH_COM);
			printf("cmd : %s\n",cmd);
			system(cmd);
		}
	}
	return parser_cnt;
}

int enum_av(const string &file_parent, const string &file_type, const string &file_newname){
	const string file_org_a = file_parent+BILI_PATH_SPLIT+"audio.m4s";
	const string file_org_v = file_parent+BILI_PATH_SPLIT+"video.m4s";
	const string file_dest = file_parent+BILI_PATH_SPLIT+"av";	// cmd has type
	if(file_exists(file_org_a)==0 || file_exists(file_org_v)==0) return 0;
	char cmd[2048*5];
	// ffmpeg trans
	if(bili_trans_cmd.length()){
		sprintf(cmd,"%s -i %c%s%c -i %c%s%c %s %c%s%s%c",
				bili_trans_tool.c_str(),
				BILI_PATH_COM,file_org_v.c_str(),BILI_PATH_COM,
				BILI_PATH_COM,file_org_a.c_str(),BILI_PATH_COM,
				bili_trans_cmd.c_str(),
				BILI_PATH_COM,file_dest.c_str(),file_type.c_str(),BILI_PATH_COM);
	}
	else{
		sprintf(cmd,BILI_FFMPEG_AV2MP4,
				bili_trans_tool.c_str(),
				file_org_v.c_str(),
				file_org_a.c_str(),
				file_dest.c_str());
	}
	printf("cmd : %s\n",cmd);
	system(cmd);
	// move
	sprintf(cmd,"%s %c%s%s%c %c%s%c",BILI_CMD_MV,
			BILI_PATH_COM,file_dest.c_str(),file_type.c_str(),BILI_PATH_COM,
			BILI_PATH_COM,file_newname.c_str(),BILI_PATH_COM);
#ifdef _WIN32
	utf8_to_gbk(cmd,cmd,sizeof(cmd));
	remove_str(cmd,"?");
#endif
	for(string fn=file_newname; file_exists(fn);){
		// duplicate file_name
		int id=(fn[fn.length()-file_type.length()-1]-'0'+300)%10+1;
		fn[fn.length()-file_type.length()-1]=id+'0';
		cmd[strlen(cmd)-file_type.length()-1]=id+'0';
	}
	printf("cmd : %s\n",cmd);
	system(cmd);
	// delete *.blv
	if(bili_action.compare("move")==0){
		sprintf(cmd,"%s %c%s%c %c%s%c",
				BILI_CMD_RM,
				BILI_PATH_COM,file_org_v.c_str(),BILI_PATH_COM,
				BILI_PATH_COM,file_org_a.c_str(),BILI_PATH_COM);
		printf("cmd : %s\n",cmd);
		system(cmd);
	}
}

int parser_json(const string &json, const string match, string &data){
	int i=json.find(match);
	if(i==json.npos) return 0;
	i=json.find("\"",i+match.length());
	if(i==json.npos) return 0;
	int j=json.find("\"",++i);
	if(j==json.npos) return 0;
	data = json.substr(i,j-i);
	return 1;
}

int main(int argc, char * argv[]){
	printf("%s [%s]\n",BILI_TOOL_VER, BILI_TOOL_OS);

	parser_param(argc-1, argv+1);
	get_current_path_abs(bili_current_dir_abs);
	trim_end(bili_source_dir,BILI_PATH_SPLIT);
	get_path_abs(bili_source_dir);
	cout<<"current dir : "<<bili_current_dir_abs<<endl
		<<"bilidown dir: "<<bili_source_dir<<endl
		<<"biliapp prop: "<<bili_property_type<<endl
		<<"bili action : "<<bili_action<<endl
		<<"trans tool  : "<<bili_trans_tool<<endl;
	list_bili_source(bili_source_dir);
	return 0;
}
